/*
  JCM is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  JCM is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with JCM.  If not, see <http://www.gnu.org/licenses/>.
*/
import java.awt.*;
import java.text.*;
import java.util.*;
import java.io.*;
import java.sql.*;
import java.math.*;
import java.util.regex.*;
import javax.swing.*;
import javax.print.DocFlavor.*;
import org.apache.axis.AxisFault;

import neo.bcb.*;

import static java.math.BigDecimal.ZERO;
import static java.math.BigDecimal.ONE;

/**
 * Implementação do "Business Data Model" do aplicativo.
*/
public class JCM extends JCMFrame
{
  /** Construtor complementar da classe estendida. */
  public JCM()
  {
    super();

    splashHelper.showTask("Conectando ao banco de dados.");

    dbConnect(); // estabelece a conexão ao DB

    splashHelper.showTask("Preparando o ambiente.");

    // loop de preenchimento do combo de índices disponíveis
    for (DBTable table: dbtables)
    {
      indicesCombo.addItem(table.name);
    }
    updateIndicesTipText();

    @SuppressWarnings("deprecation")
    // Date para 1 de janeiro de 1900
    java.util.Date minDate = new java.util.Date(0, 0, 1);
    @SuppressWarnings("deprecation")
    // Date para 31 de dezembro de 2099
    java.util.Date maxDate = new java.util.Date(199, 11, 31);
    // Configura a amplitude dos componentes seletores de datas
    iniChooser.setSelectableDateRange(minDate, maxDate);
    fimChooser.setSelectableDateRange(minDate, maxDate);

    dateUtils = DateUtils.getInstance(iniChooser.getDateFormatString());

    cf = NumberFormatFactory.getDecimalFormat("#,##0.00;'<tt>'(#,##0.00)'</tt>'");

    pf = NumberFormatFactory.getDecimalFormat();

    // assinala a localização do arquivo TrustStore
    System.setProperty("javax.net.ssl.trustStore",
      USER_DIR + FILE_SEPARATOR + "jssecacerts");

    File f = new File(System.getProperty("javax.net.ssl.trustStore"));

    // verificação do arquivo de certificados de autoridade do JSSE
    if (checkCAcerts || ! f.exists())
    {
      splashHelper.showTask("Verificando certificado.");

      /* Check-up da instalação do certificado de autoridade. */

      try {
        CAInstaller instalador = CAInstaller.getInstance();
        String host = "www3.bcb.gov.br";
        if (!instalador.check(host)) {
          splashHelper.showTask("Instalando certificado.");
          instalador.install(null, host, 443);
        }
      } catch (Exception e) {
        System.err.println(e);
      }
    }

    splashHelper.showTask("Ativando a interface.");
  }

  /**
   * Classe de consulta trivial das informações de tabela do DB
   * correspondente a uma série temporal.
  */
  private class DBTable
  {
    /**
     * Construtor do objeto.
     *
     * @param tableName Nome da tabela.
    */
    public DBTable(String tableName)
    {
      name = tableName;
      build();
    }

    /** Montagem das informações mais consultadas. */
    public void build()
    {
      size = 0;
      String q = buildSQL("status", name);
      try {
        Statement stm = connection.createStatement();
        // consulta o número de registros e amplitude das datas
        ResultSet result = stm.executeQuery(q);
        if (result.next()) {
          // obtêm o número de registros
          size = result.getInt(1);
          if (size > 0)
          {
            DateFormat f = null;
            long t = 0;
            // obtêm a data mais recente
            if (useSQLite) {
              f = new SimpleDateFormat("yyyy-MM-dd");
              t = f.parse(result.getString(3)).getTime();
            } else {
              t = result.getDate(3).getTime();
            }
            Calendar c = Calendar.getInstance();
            c.setTimeInMillis(t);
            c.set(Calendar.DATE, DateUtils.daysInMonth(c));
            c.set(Calendar.HOUR_OF_DAY, 0);
            c.set(Calendar.MINUTE, 0);
            c.set(Calendar.SECOND, 0);
            c.set(Calendar.MILLISECOND, 0);
            last = c.getTimeInMillis();

            // obtêm a data mais antiga
            t = (useSQLite ? f.parse(result.getString(2)) : result.getDate(2)).getTime();
            c.setTimeInMillis(t);
            c.set(Calendar.HOUR_OF_DAY, 0);
            c.set(Calendar.MINUTE, 0);
            c.set(Calendar.SECOND, 0);
            c.set(Calendar.MILLISECOND, 0);
            //t = c.getTimeInMillis();
            first = c.getTimeInMillis();
          }
        }
        result.close();
        stm.close();
      } catch (SQLException e) {
        e.printStackTrace();
      } catch (ParseException e) {
        System.err.println(e);
      }
    }

    /**
     * Constrói comentário sobre a amplitude da série.
     *
     * @param full Indicador da montagem de comentário completo.
     * @return Comentário sobre a amplitude da série.
    */
    public String getComment(boolean full)
    {
      if (size == 0) return "tabela vazia";
      String format = full ? "<html>série disponível de <b>%1$tB.%1$tY</b> a <b>%2$tB.%2$tY</b></html>" : "<b>%1$tB.%1$tY</b> a <b>%2$tB.%2$tY</b>";
      return String.format(DateUtils.localeBR, format, first, last);
    }

    /** Nome da tabela ou índice de correção monetária */
    private String name;

    /** UNIX timestamp da data inicial da série. */
    private long first;

    /** UNIX timestamp da data final da série. */
    private long last;

    /** Número de registros :: número de observações da série */
    private int size;
  }

  /** Array de informações sobre cada tabela no DB. */
  private DBTable[] dbtables;

  /** Conexão única ao DB. */
  private Connection connection;

  /** Indicador de uso do SQLite devido a ausência do tipo DATE. */
  private boolean useSQLite;

  /** Nome do arquivo resource do esquema do DB. */
  private String dbScheme;

  /**
   * Complementa o carregamento das propriedades comportamentais
   * obtendo o esquema de DB preservado em sessão anterior, cujo
   * valor default implica no uso do SQLite.
  */
  @Override protected void loadBehavior()
  {
    super.loadBehavior();

    dbScheme = properties.getProperty("dbScheme", "db03.properties");
  }

  /**
   * Complementa o mecanismo de persistência das propriedades
   * comportamentais preservando o esquema de DB.
  */
  @Override protected void saveBehavior()
  {
    properties.put("dbScheme", dbScheme);

    super.saveBehavior();
  }

  /**
   * Conecta ao banco de dados e obtêm informações sobre as tabelas
   * preenchendo o componente da interface para escolha de índice
   * de correção monetária.
  */
  protected void dbConnect()
  {
    if (! dbScheme.matches("(?i:^[a-z]\\w*\\.properties$)"))
    {
      JOptionPane.showMessageDialog(null,
        "<html>A execução será abortada.<br><br>Esquema de banco de dados mal declarado.</hmtl>",
        "Erro Fatal", JOptionPane.ERROR_MESSAGE);
      System.exit(1);
    }

    // carrega as propriedades do esquema de DB reaproveitando
    // o único objeto do tipo com escopo global e hereditário
    properties = loadProperties(dbScheme);

    try {
      // carregamento dinâmico da classe JDBC específica do esquema
      Class.forName(peek(properties, "driverClassName"));

      useSQLite = (peek(properties, "protocol").indexOf("sqlite") != -1);

      // monta a url de acesso ao DB
      String urlDB = String.format("%s%s%s",
        peek(properties, "protocol"),
        peek(properties, "serverName"),
        peek(properties, "dbFileName"));

      String username = null;
      String password = null;
      if (!useSQLite)
      {
        // acesso ao username e password
        Properties safe = /* neo.crypto.Tools. */ loadProperties(JCMDB_PROPERTIES);
        username = peek(safe, "username");
        password = peek(safe, "password");
      }
      // estabelece conexão ao DB
      connection = DriverManager.getConnection(urlDB, username, password);

      DatabaseMetaData dbmd = connection.getMetaData();

      // somente métodos naive funcionam bem com qualquer marca/versão
      // de DB e driver

      String dbName = useSQLite ? peek(properties, "dbCatalogName") : null;

      // Importante: Derby usa "username" como "schema pattern"
      //             portanto compatibilize o DB com a regra.

      String schemaPattern = (peek(properties, "protocol").indexOf("derby") != -1) ? username.toUpperCase() : "PUBLIC";

      final String namePattern = "%";

      final String[] types = {"GLOBAL TEMPORARY", "SYSTEM TABLE", "TABLE", "VIEW"};

      final String FMT = "_%d_";

      int len = 0; // contador de tabelas válidas

      // obtêm metadata das tabelas no DB
      ResultSet rs = dbmd.getTables(dbName, schemaPattern, namePattern, types);
      // loop de pesquisa nas tabelas
      while (rs.next())
      {
        // obtêm o nome da tabela
        //
        // SQLite BUG: em letras MAIÚSCULAS
        // Workaround: obter os nomes das tabelas via statement
        // "SELECT NAME FROM sqlite_master WHERE TYPE='TABLE'"
        //
        String tableName = rs.getString(3);
        // obtêm metadata das colunas desta tabela
        ResultSet r = dbmd.getColumns(dbName, schemaPattern, tableName, namePattern);
        // pesquisa campos especificados na tabela
        int n = 0;
        while (r.next() && (n < 2))
        {
          String field = r.getString(4).toUpperCase();
          String type = r.getString(6).toUpperCase();
          // checa se nome e tipo de cada campo coincidem com especificação
          if ((field.equals("DATA") && type.equals("DATE"))
              || (field.equals("VALOR") && type.contains("DOUBLE"))) n++;
        }
        r.close();
        // se encontrou os campos necessários então associa chave
        // ao nome da tabela compatível com especificações
        if (n == 2) {
          // temporariamente adiciona o "par" às propriedades
          properties.setProperty(String.format(FMT, len++), tableName);
        }
      }
      rs.close();

      // checa o número de tabelas válidas
      if (len == 0)
      {
        dbCloseConnection();
        JOptionPane.showMessageDialog(null,
          "<html>A execução será abortada.<br><br>O banco de dados não contém tabelas em<br>conformidade com as especificações.</html>",
          "Erro Fatal", JOptionPane.ERROR_MESSAGE);
        System.exit(1);
      }

      dbtables = new DBTable[len];
      // loop de montagem do array de informações de tabelas
      while (len-- > 0)
      {
        String key = String.format(FMT, len);
        dbtables[len] = new DBTable(properties.getProperty(key));
        // remove o "par" adicionado temporariamente
        properties.remove(key);
      }

      properties.setProperty("nameAndVersion", String.format("%s %s", dbmd.getDatabaseProductName(), dbmd.getDatabaseProductVersion()));

    } catch (ClassNotFoundException e) {
      System.err.println(e);
      JOptionPane.showMessageDialog(null,
        "Driver do banco de dados não encontrado.",
        "Erro Fatal", JOptionPane.ERROR_MESSAGE);
      System.exit(1);
    } catch (SQLException e) {
      System.out.print("FATAL ERROR: " + e);
      String message = null;
      if (e.toString().contains("Unable to connect")) {
        message = "FALHA DE CONEXÃO AO BANCO DE DADOS.";
      } else {
        message = "ERRO DE ACESSO AO BANCO DE DADOS.";
      }
      JOptionPane.showMessageDialog(null, message, "Erro Fatal", JOptionPane.ERROR_MESSAGE);
      System.exit(1);
    }

    // organiza acesso às tabelas em ordem decrescente de última data
    // e ordem crescente de primeira data em caso de empates
    Arrays.sort(dbtables,
      new Comparator<DBTable>() {
        public int compare(DBTable t0, DBTable t1) {
          return t0.last > t1.last ? -1
            : (t0.last < t1.last ? 1
              : (t1.first < t0.first ? 1 : (t1.first > t0.first ? -1 : 0)));
        }
      });
  }

  /**
   * Invoca diálogo modal de edição da tabela de banco de dados selecionada.
  */
  protected void editTable()
  {
    int ndx = indicesCombo.getSelectedIndex();
    EditWindow w = new EditWindow(this, connection, dbtables[ndx].name, properties);
    centerDialog(w);
    w.setVisible(true);
    // libera recursos do sistema
    w.dispose();
    // reconstrói informações sobre a tabela
    dbtables[ndx].build();
    // atualiza o texto de ajuda contextual correspondente
    updateIndicesTipText();
  }

  /**
   * Invoca o diálogo modal de criação de tabela de banco de dados.
   * O nome da tabela/índice deve começar com letra, contendo apenas
   * letras/digitos/hífen/underline e não pode coincidir com nomes de
   * tabelas existentes no banco de dados, independente do uso de
   * letras capitais.
  */
  protected void createTable()
  {
    Properties q = loadProperties(INDICES_MENSAIS_PROPERTIES);
    String[] lista = q.values().toArray(new String[0]);

    NewTableDialog dialog = new NewTableDialog(this, lista);
    centerDialog(dialog);
    dialog.setVisible(true);
    // obtêm a reposta do diálogo
    boolean ok = dialog.okPressed();
    String name = dialog.getTableName();
    String serieName = dialog.getIndiceName();
    // libera recursos do sistema
    dialog.dispose();

    if (!ok || (name == null)) return;

    // checa se o nome proposto adere ao padrão imposto.
    name = name.trim();
    if (! name.matches("(?i:^[a-z][\\w-]*$)"))
    {
      JOptionPane.showMessageDialog(getContentPane(),
        String.format("<html>O texto <b>\"%s\"</b> não serve como nome de tabela.</html>", name),
        "Manutenção do Banco de Dados", JOptionPane.ERROR_MESSAGE);
      return;
    }

    // checa se o nome proposto é usado como nome de outra tabela
    ok = false;
    for (int j=0; !ok && (j < dbtables.length); j++) {
      ok = name.equalsIgnoreCase(dbtables[j].name);
    }
    if (ok) {
      JOptionPane.showMessageDialog(getContentPane(),
        String.format("<html>O texto <b>\"%s\"</b> já está em uso como nome de tabela.</html>", name),
        "Manutenção do Banco de Dados", JOptionPane.ERROR_MESSAGE);
      return;
    }

    // executa a criação de tabela de índice de correção monetária
    try {
      Statement stm = connection.createStatement();
      stm.execute(buildSQL("createTable", name));
      stm.close();
    } catch (SQLException e) {
      System.err.println(e);
      JOptionPane.showMessageDialog(getContentPane(),
        String.format("<html>Não foi possível criar<br>a tabela <b>\"%s\"</b>.</html>", name),
        "Manutenção do Banco de Dados", JOptionPane.ERROR_MESSAGE);
      return;
    }

    // atualiza a lista de informações das tabelas disponíveis
    int ndx = dbtables.length;
    DBTable[] list = new DBTable[ndx + 1];
    for (int j=0; j < ndx; j++) list[j] = dbtables[j];
    dbtables = list;
    // monta as informações da nova tabela
    dbtables[ndx] = new DBTable(name);
    // atualiza o comboBox de seleção de índice de correção monetária
    indicesCombo.addItem(name);
    // seleciona o novo índice e entra em modo de edição da tabela
    indicesCombo.setSelectedIndex(ndx);

    // checa se a tabela foi associada a uma série temporal do SGS do BCB
    if (! serieName.startsWith("<")) {
      String bcbcode = null;
      // obtêm o código correspondente ao nome da série escolhida
      for (Enumeration<?> en = q.propertyNames(); en.hasMoreElements();)
      {
        bcbcode = (String) en.nextElement();
        if (q.getProperty(bcbcode).equals(serieName)) break;
      }
      // atualiza as propriedades de atualização online das séries
      Properties p = loadProperties(DBUPDATE_PROPERTIES);
      p.put(dbtables[ndx].name, bcbcode);
      saveProperties(p, DBUPDATE_PROPERTIES);
    }

  }

  /**
   * Invoca diálogo modal de eliminação da tabela de banco de dados
   * selecionada.
  */
  protected void dropTable()
  {
    // checa o número mínimo de tabelas disponíveis
    if (dbtables.length == 1) {
      JOptionPane.showMessageDialog(getContentPane(),
        "<html>Operação não permitida.<br><br>O <b>JCM</b> requer ao menos uma tabela de dados<br>para executar as atualizações.</html>",
        "Manutenção do Banco de Dados", JOptionPane.WARNING_MESSAGE);
      return;
    }

    // obtêm o índice da tabela selecionada a eliminar
    final int ndx = indicesCombo.getSelectedIndex();

    // invoca o diálogo de confirmação
    final int option = JOptionPane.showConfirmDialog(getContentPane(),
      String.format("<html>Os dados da tabela <b>\"%s\"</b><br>ficarão  <b>permanentemente indisponíveis</b>.<br>Confirma a eliminação da tabela?", dbtables[ndx].name),
      "Manutenção do Banco de Dados", JOptionPane.YES_NO_OPTION);

    if (option == JOptionPane.YES_OPTION)
    {
      // elimina a tabela no banco de dados
      try {
        Statement stm = connection.createStatement();
        stm.execute(buildSQL("dropTable", dbtables[ndx].name));
        stm.close();
      } catch (SQLException e) {
        System.err.println(e);
        JOptionPane.showMessageDialog(getContentPane(),
          String.format("<html>Não foi possível eliminar<br>a tabela <b>\"%s\"</b>.</html>", dbtables[ndx].name),
          "Manutenção do Banco de Dados", JOptionPane.ERROR_MESSAGE);
        return;
      }

      // carrega as propriedades de atualização online das séries
      // para remoção da chave e valor correspondentes a tabela eliminada
      Properties p = loadProperties(DBUPDATE_PROPERTIES);
      String key;
      if (!p.isEmpty()
          && (p.containsKey(key = dbtables[ndx].name)
              //
              // This is a workaround for a bug in SQLite driver that
              // returns uppercase of table names via MetaData.
              //
              || p.containsKey(key = key.toLowerCase())))
      {
        // atualiza/salva as propriedades
        p.remove(key);
        saveProperties(p, DBUPDATE_PROPERTIES);
      }

      // atualiza a lista de informações das tabelas disponíveis
      int len = dbtables.length;
      DBTable[] list = new DBTable[len - 1];
      for (int k=-1, j=0; j < len; j++)
      {
        if (j != ndx) list[++k] = dbtables[j];
      }
      dbtables = list;

      // atualiza o comboBox de seleção de índice
      indicesCombo.removeItemAt(ndx);
    }
  }

  /**
   * Atualização on line da tabela selecionada.
  */
  protected void updateTable()
  {
    int ndx = indicesCombo.getSelectedIndex();

    // carrega as propriedades de atualização online das séries
    Properties p = loadProperties(DBUPDATE_PROPERTIES);

    String bcbcode = p.isEmpty() ? null : p.getProperty(dbtables[ndx].name);

    // tenta obter o código da série dialogando com o usuário se necessário
    if (bcbcode == null) return;

    try {

      Aurea aurea = Aurea.getInstance();

      // obtêm o último valor observado da série
      Aurea.VO vo = aurea.get(bcbcode);
      long t = vo.getDate().getTime();

      // checa se é possível atualizar a tabela corrente
      if ((dbtables[ndx].size > 0) && (t <= dbtables[ndx].last))
      {
        JOptionPane.showMessageDialog(getContentPane(),
          String.format("<html>A tabela \"%s\" está atualizada.</html>", dbtables[ndx].name),
          "Atualização On Line", JOptionPane.INFORMATION_MESSAGE);
        return;
      }

      setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));

      // prepara o SQL de inserção
      String q = buildSQL("insert", dbtables[ndx].name);
      PreparedStatement ps = connection.prepareStatement(q);

      Calendar c = Calendar.getInstance();
      if (dbtables[ndx].size > 0) {
        // calcula a data posterior a última data da tabela corrente
        c.setTimeInMillis(dbtables[ndx].last);
        c.set(Calendar.DATE, 1);
        c.add(Calendar.MONTH, 1);
      } else {
        // obtêm a data da 1ª observação disponível no db do servidor
        WSSerieVO w = aurea.stub.getUltimoValorVO(Long.parseLong(bcbcode));
        c.set(Calendar.YEAR, w.getAnoInicio());
        c.set(Calendar.MONTH, w.getMesInicio());
        c.set(Calendar.DATE, w.getDiaInicio());
        c.set(Calendar.HOUR_OF_DAY, 0);
        c.set(Calendar.MINUTE, 0);
        c.set(Calendar.SECOND, 0);
        c.set(Calendar.MILLISECOND, 0);
      }
      long i = c.getTime().getTime();
      // checa se é necessário atualizar aquém da atual
      if (i < t)
      {
        String di = dateUtils.format(c);
        // calcula a data anterior a última disponível no servidor
        c.setTimeInMillis(t);
        c.add(Calendar.MONTH, -1);
        String df = dateUtils.format(c);

        // obtêm as observações prévias da série
        Object[] serie = aurea.get(bcbcode, di, df);
        // loop de inserção das observações prévias
        for (Object oo: serie)
        {
          Aurea.VO item = (Aurea.VO) oo;
          t = item.getDate().getTime();
          java.sql.Date date = new java.sql.Date(t);
          if (useSQLite) ps.setString(1, date.toString());
          else ps.setDate(1, date);
          ps.setDouble(2, new Double(item.getValue().doubleValue() / 100.0));
          ps.executeUpdate();
        }
      }
      // inserção da observação mais atual
      t = vo.getDate().getTime();
      java.sql.Date date = new java.sql.Date(t);
      if (useSQLite) ps.setString(1, date.toString());
      else ps.setDate(1, date);
      ps.setDouble(2, new Double(vo.getValue().doubleValue() / 100.0));
      ps.executeUpdate();

      // reconstrói informações sobre a tabela
      dbtables[ndx].build();
      // atualiza o texto de ajuda contextual correspondente
      updateIndicesTipText();

      String message = (i < t) ? String.format("<html><center>A série temporal do \"%1$s\" foi atualizada<br>com observações de %2$tb/%2$tY a %3$tb/%3$tY.</center></html>", dbtables[ndx].name, i, t) : String.format("<html><center>A série temporal do \"%1$s\" foi atualizada<br>com a observação de %2$tb/%2$tY.</center></html>", dbtables[ndx].name, t);

      JOptionPane.showMessageDialog(getContentPane(), message, "Atualização On Line", JOptionPane.INFORMATION_MESSAGE);

    } catch (SQLException e) {
      System.err.println(e);
    } catch (Exception e) {
      // satisfação a exceções relacionadas com as requisições ao webservice
      String fault = null;
      if (e instanceof AxisFault) {
        fault = ((AxisFault) e).getFaultString();
        if (fault.startsWith("javax.net.ssl")) {
          try {
            CAInstaller.getInstance().install(null, "www3.bcb.gov.br", 443);
          } catch (Exception xe) {
            System.out.println(xe);
          }
          if (fault.contains("SSLHandshakeException"))
            // CA presente no keystore, porém não reconhecido pelo server (revogado)
            // portanto foi substituído pela versão mais recente
            fault = "Certificado de autoridade foi atualizado.";
          else
            // keystore não contém CA do BCB ou não existe seu arquivo container
            // portanto foi instalado e armazenado
            fault = "Certificado de autoridade foi instalado.";
          // infelizmente não há alternativa
          fault += "<br><br>Reinicie o aplicativo para<br>acessar o webservice por favor.";
        } else {
          fault = (fault.length() == 0) ? "Erro desconhecido do web service." : fault.substring(fault.lastIndexOf(':')+2);
        }
      } else if (e instanceof java.rmi.RemoteException) {
        fault = "O servidor está fora do ar.<br>Tente atualizar mais tarde.";
      } else {
        fault = "Erro desconhecido.";
      }
      JOptionPane.showMessageDialog(getContentPane(),
        String.format("<html><center>%s</center></html>", fault),
        "Atualização On Line", JOptionPane.ERROR_MESSAGE);
      System.err.println(e);
    } finally {
      setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
    }
  }

  /** Encerra a conexão ao DB. */
  protected void dbCloseConnection()
  {
    String protocol = properties.getProperty("protocol");
    try {
      // checa uso do Derby DB
      if (protocol.indexOf("derby") != -1) {
        connection.close();
        DriverManager.getConnection("jdbc:derby:;shutdown=true");
      } else {
        // checa uso do HSQLDB
        if (protocol.indexOf("hsqldb") != -1) {
          connection.createStatement().execute(
            peek(properties, "shutdown"));
        }
        connection.close();
      }
    } catch (SQLException e) {
      System.err.println(e);
    }
  }

  /** Atualiza o texto de ajuda contextual extraído do DB. */
  protected void updateIndicesTipText()
  {
    // evita a falta de sincronia ao eliminar item do combo
    EventQueue.invokeLater(new Runnable() {
      public void run()
      {
        int ndx = indicesCombo.getSelectedIndex();
        indicesCombo.setToolTipText(dbtables[ndx].getComment(true));
      }
    });
  }

  /** Monta um documento reportando o status operacional do DB. */
  protected void makeDbReport()
  {
    setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
    Temporizador temporizador = new Temporizador(progressBar);

    StringBuilder sb = new StringBuilder("<html><head>");
    sb.append("<title>Análise Operacional do Banco de Dados</title>");
    sb.append("<style>").append( loadText("dbreport.css") );
    sb.append("</style></head><body>");
    sb.append("<h1>Análise Operacional do Banco de Dados</h1>");

    DateFormat f = useSQLite ? new SimpleDateFormat("yyyy-MM-dd"): null;

    try {
      sb.append("<div>RDBMS: <b>");
      sb.append(properties.getProperty("nameAndVersion"));
      sb.append("</b></div>");

      sb.append("<div><table cellpadding=2 cellspacing=0>");
      sb.append("<tr><td class=first>Nome Catálogo:</td><td>");
      sb.append(peek(properties, "dbCatalogName"));
      sb.append("</td></tr>");
      sb.append("<tr><td class=first>No.Tabelas:</td><td>");
      sb.append(dbtables.length).append("</td></tr></table></div>");

      // Nota: O SQLite não suporta resultset do tipo scrollable.
      Statement stm = connection.createStatement();

      Calendar calendar = Calendar.getInstance();

      int n = 1;
      temporizador.start();

      Properties r = loadProperties(INDICES_MENSAIS_PROPERTIES);
      Properties p = loadProperties(DBUPDATE_PROPERTIES);

      // atravessa o array de tabelas
      for (DBTable table: dbtables)
      {
        sb.append("<div>[").append( n ).append("] <b>");
        sb.append(table.name);
        // apresenta o nome da tabela/índice
        sb.append("</b><table class=info cellpadding=2 cellspacing=1 border=0>");

        String bcbcode = p.getProperty(table.name);
        if (bcbcode != null)
        {
          sb.append("<tr><td nowrap class=first>Código no SGS do BCB:</td><td>");
          sb.append(bcbcode).append("</td></tr>");
          sb.append("<tr><td nowrap class=first>Descrição no SGS do BCB:</td><td valign=top>");
          sb.append(r.getProperty(bcbcode)).append("</td></tr>");
        }

        // apresenta o número de registros
        sb.append("<tr><td nowrap class=first>No.Registros:</td><td>");
        sb.append(table.size).append("</td></tr>");

        if (table.size > 0)
        {
          // apresenta a amplitude da série temporal
          sb.append("<tr><td nowrap class=first>Período:</td><td>");
          sb.append(table.getComment(false)).append("</td></tr>");

          // obtêm a sequência de datas da série temporal
          String q = buildSQL("allDates", table.name);
          ResultSet result = stm.executeQuery(q);
          if (result.next())
          {
            // preparação de check-up da continuidade da série temporal
            sb.append("<tr><td nowrap valign=top class=first>Conclusão:</td>");
            StringBuilder errList = null;
            int errCounter = 0;
            // obtêm a primeira data
            long previous = (useSQLite ? f.parse(result.getString(1)) : result.getDate(1)).getTime();
            // atravessa o resultset sequencialmente
            while (result.next())
            {
              long current = (useSQLite ? f.parse(result.getString(1)) : result.getDate(1)).getTime();
              calendar.setTimeInMillis(current);
              // obtêm a expectativa da data prévia
              calendar.add(Calendar.MONTH, -1);
              // compara os milisegundos das datas
              if (calendar.getTimeInMillis() != previous)
              {
                if (errCounter++ == 0) {
                  errList = new StringBuilder();
                  errList.append("Descontinuidade no(s) período(s):")
                    .append("<table cellpadding=5 cellspacing=5 border=0>");
                }
                errList.append("<tr><td nowrap class=error>&raquo;&nbsp;")
                  .append(String.format(DateUtils.localeBR, "%1$tB de %1$tY", previous))
                  .append("</td></tr>");
              }
              previous = current;
            }
            if (errCounter > 0) {
              sb.append("<td nowrap class=warn>").append(errList).append("</table>");
            } else {
              // não há furos na série temporal
              sb.append("<td nowrap>série temporal sem descontinuidade.");
            }
            sb.append("</td></tr>");
          }
          result.close();
        }

        sb.append("</table></div>");

        temporizador.update(((double) (n++)) / dbtables.length);
      }
      stm.close();

    } catch (ParseException e) {
      System.err.println(e);
    } catch (SQLException e) {
      System.err.println(e);
    }
    sb.append("<div class=footer>");
    sb.append(String.format(DateUtils.localeBR, "Gerado em %tc.", System.currentTimeMillis()));
    sb.append("</div>").append("</body></html>");

    clearReport();
    report.getEditorKit().createDefaultDocument();
    report.setContentType("text/html");
    report.setText(sb.toString());
    assureSplitPaneDisplay(true);
    report.setCaretPosition(0);
    report.requestFocusInWindow();
    setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
    temporizador.terminate();
  }

  /**
   * Monta expressão SQL correspondente à chave e nome de tabela fornecidos.
   *
   * @param key Chave da expressão SQL.
   * @param tableName Nome da tabela a consultar.
   * @return A expressão SQL pronta para consulta.
  */
  private String buildSQL(String key, String tableName)
  {
    return properties.getProperty(key).replace("%s", tableName);
  }

  /**
   * Ajusta o formatador de valores monetários conforme indicação de
   * uso/apresentação dos centavos.
   *
   * @param useCents Indicador de uso dos centavos.
  */
  private void adjustFormater(boolean useCents)
  {
    this.useCents = useCents;
    if (useCents) {
      cf.setMaximumFractionDigits(2);
      cf.setMinimumFractionDigits(2);
    } else {
      cf.setMaximumFractionDigits(0);
    }
  }

  /**
   * Cálculo de taxas proporcionais.
   *
   * @param x Taxa objeto do cálculo.
   * @param a Numerador da proporção.
   * @param b Denominador da proporção.
  */
  private BigDecimal ratio(BigDecimal x, int a, int b)
  {
    return (a == b) ? x : x.multiply((new BigDecimal(a)).divide(new BigDecimal(b), mc));
  }

  /**
   * Monta linha descritiva da planilha reportando o capital
   * ou os juros remuneratórios pendentes.
   *
   * @param cssClassName Nome da classe CSS usada na linha da tabela HTML.
   * @param dateString Texto contendo a data da operação.
   * @param descricao Texto contendo a descrição da operação.
   * @param value Valor monetário do capital ou dos juros pendentes.
  */
  private void mkRow(String cssClassName, String dateString, String descricao, BigDecimal value)
  {
    sw.format("<tr><td class=%1$s_center>%2$s</td><td nowrap colspan=7 class=%1$s_center>%3$s</td><td class=%1$s>%4$s</td></tr>", cssClassName, dateString, descricao, cf.format(value));
  }

  /**
   * Monta linha descritiva da planilha reportando o capital.
   *
   * @param cssClassName Nome da classe CSS usada na linha da tabela HTML.
   * @param dateString Texto contendo a data da operação.
   * @param descricao Texto contendo a descrição da operação.
  */
  private void mkRow(String cssClassName, String dateString, String descricao)
  {
    mkRow(cssClassName, dateString, descricao, kptal);
  }

  /**
   * Realiza reforma monetária aplicando transformação de escala.
   *
   * @param r Índice da reforma monetária.
  */
  private void scale(int r)
  {
    kptal = Reforma.reformas[r].transform(kptal);
    kptal = kptal.setScale(2, RoundingMode.HALF_UP);
    okane = Reforma.reformas[r].transform(okane);
    okane = okane.setScale(2, RoundingMode.HALF_UP);
    scorr = Reforma.reformas[r].transform(scorr);
    scorr = scorr.setScale(2, RoundingMode.HALF_UP);
    sjuros = Reforma.reformas[r].transform(sjuros);
    sjuros = sjuros.setScale(2, RoundingMode.HALF_UP);
    jurosPending = Reforma.reformas[r].transform(jurosPending);
    jurosPending = jurosPending.setScale(2, RoundingMode.HALF_UP);

    adjustFormater(Reforma.reformas[r].getUseCents());

    currency.next(); // avança para a moeda seguinte

    // adiciona linha à planilha reportando a reforma
    mkRow("reforma", dateUtils.format(Reforma.reformas[r].getDate()), Reforma.reformas[r].getComment(), kptal);
  }

  /**
   * Calcula/acumula/agrega taxas de juros remuneratórios e correção
   * monetária proporcionais ao número de dias num mês além dos demais
   * valores relacionados, reportando todos numa nova linha da planilha.
   *
   * @param cssClassName Nome da classe CSS usada na linha da tabela HTML.
   * @param dateString Texto contendo a data da operação.
   * @param nDays Número de dias entre a data dessa operação e da data
   *              da operação anterior.
   * @param txMensal Taxa de correção monetária no mês dessa operação.
   * @param daysInMonth Número de dias no mês dessa operação.
   * @param daysInYear Número de dias no ano dessa operação.
  */
  private void jcm(String cssClassName, String dateString, int nDays, BigDecimal txMensal, int daysInMonth, int daysInYear)
  {
    // Juros remuneratórios calculados antes da correção monetária
    // para evitar contaminação ao agregar a correção ao capital.

    // taxa de juros remuneratórios proporcional
    // ao número de dias efetivos no mês
    BigDecimal jrate = null;
    // valor do juros no mês
    BigDecimal juros = null;
    // verifica a efetividade de uso da taxa PRO-RATA
    if (validProRata) {
      // taxa PRO-RATA DIE * número de dias efetivos no mês
      jrate = ratio(proRata, nDays, daysInYear);
      // acumula a taxa de juros proporcional
      jurosAno = jurosAno.add(jrate);
      // calcula o valor do juros no mês
      juros = kptal.multiply(jrate);
      //
      if (!useCents) juros = juros.divideToIntegralValue(ONE);
      else juros = juros.setScale(2, RoundingMode.HALF_UP);
      // acumula o valor do juros ao seu somatório
      sjuros = sjuros.add(juros);
      sjuros = sjuros.setScale(2, RoundingMode.HALF_UP);
      // acumula o juros pendente até a data de agregação
      jurosPending = jurosPending.add(juros);
      jurosPending = jurosPending.setScale(2, RoundingMode.HALF_UP);
    }

    // storage da taxa mensal de correção monetária proporcional
    BigDecimal rate = null;
    // storage do valor da correção monetária obtida com a taxa proporcional
    BigDecimal correcao = null;
    // verifica a efetividade de uso da taxa mensal de correção monetária
    if (txMensal != null) {
      // taxa de correção monetária proporcional ao número
      // de dias efetivos no mês
      rate = ratio(txMensal, nDays, daysInMonth);
      // acumula a taxa de correção monetária proporcional
      corrAcum = corrAcum.multiply(rate.add(ONE));
      // calcula o valor da correção monetária no mês
      correcao = kptal.multiply(rate);
      //
      if (!useCents) correcao = correcao.divideToIntegralValue(ONE);
      else correcao = correcao.setScale(2, RoundingMode.HALF_UP);
      // acumula o valor da correção monetária ao seu somatório
      scorr = scorr.add(correcao);
      scorr = scorr.setScale(2, RoundingMode.HALF_UP);
      // agregação da correção monetária ao capital
      kptal = kptal.add(correcao);
      kptal = kptal.setScale(2, RoundingMode.HALF_UP);
    }

    String s = String.format("<td nowrap class=%s_center>%%s</td>",  cssClassName);
    sw.write("<tr>");
    sw.format(s, dateString);
    sw.format(s, nDays);
    String t = String.format("<td nowrap class=%s>%%s</td>", cssClassName);
    if (txMensal == null) sw.format(s, "<tt>N/D</tt>");
    else sw.format(t, pf.format(txMensal));
    sw.format(t, pf.format(rate != null ? rate : 0));
    sw.format(t, cf.format(correcao != null ? correcao : 0));
    sw.format(t, pf.format(jrate != null ? jrate : 0));
    sw.format(t, cf.format(juros != null ? juros : 0));
    sw.format(t, cf.format(jurosPending));
    sw.format(t, cf.format(kptal));
    sw.write("</tr>");
  }

  /**
   * O método anterior recebendo a data como Calendar.
   *
   * @param cssClassName Nome da classe CSS usada na linha da tabela HTML.
   * @param data Data da operação do tipo Calendar.
   * @param nDays Número de dias entre a data dessa operação e da data
   *              da operação anterior.
   * @param txMensal Taxa de correção monetária no mês dessa operação.
   * @param daysInMonth Número de dias no mês dessa operação.
   * @param daysInYear Número de dias no ano dessa operação.
  */
  private void jcm(String cssClassName, Calendar data, int nDays, BigDecimal txMensal, int daysInMonth, int daysInYear)
  {
    jcm(cssClassName, dateUtils.format(data), nDays, txMensal, daysInMonth, daysInYear);
  }

  /**
   * Gera um documento text/html contendo a planilha de atualização
   * monetária mês a mês detalhando taxas/valores proporcionais às datas
   * contemplando as reformas econômicas precisamente, respeitando a
   * legislação e recomendações nos respectivos cálculos com a máxima
   * precisão possível.
   * Durante a montagem do documento atualiza a barra de progresso e ao
   * final exibe o documento no painel de reportagem.
   *
   * @return Status da geração do documento.
  */
  protected boolean makeSpreadsheet()
  {
    kptal = new BigDecimal(((Number) vini.getValue()).doubleValue());

    if (kptal.compareTo(ZERO) <= 0)
    {
      assureSplitPaneDisplay(false);
      String msg = (kptal == null) ? "<html>Digite o <b>Valor</b> a atualizar.</html>" : "<html><b>Valor</b> a atualizar<br>precisa ser maior que <b>ZERO</b>.</html>";
      JOptionPane.showMessageDialog(getContentPane(),
        msg, "Montagem de Planilha", JOptionPane.ERROR_MESSAGE);
      vini.requestFocusInWindow();
      vini.selectAll();
      return false;
    }

    JTextField tf = (JTextField) iniChooser.getDateEditor().getUiComponent();
    final Calendar di = dateUtils.parse(tf.getText());
    if (di == null)
    {
      assureSplitPaneDisplay(false);
      JOptionPane.showMessageDialog(getContentPane(),
        "<html><b>Data Inicial</b> mal declarada.</html>",
        "Montagem de Planilha", JOptionPane.ERROR_MESSAGE);
      tf.requestFocusInWindow();
      return false;
    }

    if (di.get(Calendar.YEAR) < 1900)
    {
      int response = JOptionPane.showConfirmDialog(getContentPane(),
        "<html><center>O ano da data inicial<br>é anterior a <b>1900</b>.<br>Continuar cálculos?</center></html>",
        "Confirmação", JOptionPane.YES_NO_OPTION);
      if (response != JOptionPane.YES_OPTION) {
        setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
        return false;
      }
    }

    tf = (JTextField) fimChooser.getDateEditor().getUiComponent();
    final Calendar df = dateUtils.parse(tf.getText());
    if (df == null)
    {
      assureSplitPaneDisplay(false);
      JOptionPane.showMessageDialog(getContentPane(),
        "<html><b>Data Final</b> mal declarada.</html>",
        "Montagem de Planilha", JOptionPane.ERROR_MESSAGE);
      tf.requestFocusInWindow();
      return false;
    }

    if (df.get(Calendar.YEAR) < 1900)
    {
      int response = JOptionPane.showConfirmDialog(null,
        "<html><center>O ano da data final<br>é anterior a <b>1900</b>.<br>Continuar cálculos?</center></html>",
        "Confirmação", JOptionPane.YES_NO_OPTION);
      if (response != JOptionPane.YES_OPTION) {
        setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
        return false;
      }
    }

    // verifica se a data inicial é posterior ou igual à data final
    if (!di.before(df))
    {
      assureSplitPaneDisplay(false);
      JOptionPane.showMessageDialog(getContentPane(),
        "<html><b>Data Inicial</b><br>não anterior à <b>Data Final</b>.</html>",
        "Montagem de Planilha", JOptionPane.ERROR_MESSAGE);
      tf.requestFocusInWindow();
      return false;
    }

    final int NUM_ITER = DateUtils.getNumIteracoes(di, df);

    assert (NUM_ITER > 0) : "NUM_ITER <= 0";

    // notifica o usuário sobre o número de meses no planilhamento
    if (warningsEnabled && (NUM_ITER > 120))
    {
      int response = JOptionPane.showConfirmDialog(null,
        "<html><center>O número de meses relevantes<br>no período é superior a <b>120</b>.<br>Continuar cálculos?</center></html>",
        "Confirmação",
        JOptionPane.YES_NO_OPTION);
      if (response != JOptionPane.YES_OPTION) {
        setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
        return false;
      }
    }

    // número de ordem da tabela do índice de correção monetária selecionada
    final int ndx = indicesCombo.getSelectedIndex();

    // notifica o usuário sobre a impossibilidade de prover
    // todos os dados no intervalo entre as datas assinaladas
    if (warningsEnabled
        && ((di.getTimeInMillis() < dbtables[ndx].first)
            || (df.getTimeInMillis() > dbtables[ndx].last)))
    {
      JOptionPane.showMessageDialog(getContentPane(),
        "<html>&nbsp;Aos índices de inflação com<br>datas fora do período registrado<br><u>serão atribuidos o valor</u> <b>ZERO</b>,<br>portanto não serão cálculadas as<br>correções monetárias nestas datas.</html>",
        "Montagem de Planilha", JOptionPane.WARNING_MESSAGE);
    }

    this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));

    // taxa PRO-RATA :: taxa de juros remuneratórios anual
    proRata = TAXA.eqValue(InterestRate.AO_ANO);

    // efetividade da taxa de juros neste planilhamento
    validProRata = (proRata.compareTo(ZERO) > 0);

    // verifica se a taxa de juros é superior a 100% ao ano
    // requisitando confirmação do usuário para continuar
    if (warningsEnabled && validProRata && (proRata.compareTo(ONE) > 0))
    {
      int response = JOptionPane.showConfirmDialog(null,
        "<html><center>A taxa de juros ao ano<br>é superior a <b>100%</b>.<br>Continuar cálculos?</center></html>",
        "Confirmação",
        JOptionPane.YES_NO_OPTION);
      if (response != JOptionPane.YES_OPTION) {
        setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
        return false;
      }
    }

    // cronômetro do tempo total de execução
    Chrono execChrono = new Chrono();
    execChrono.start();
    playAudioClip(6);

    ((DecimalFormat) pf).applyPattern("#,##0.###%");

    // inicia montagem do documento html
    sw = new CustomWriter();
    sw.write("<html><head><title>Atualização de Valor Monetário</title>");
    sw.append("<style>").append(loadText("spreadsheet.css").toString());
    sw.write("</style></head><body>");
    sw.write("<h1>Atualização de Valor Monetário</h1>");

    // instancia o mecanismo de pesquisa de moedas nas datas
    currency = Moeda.getInstance();

    // pesquisa a moeda para a data inicial
    currency.search(di);
    // ajusta o formatador de valores para a data inicial
    adjustFormater(Reforma.useCentsFrom(di));
    if (! useCents) kptal = kptal.divideToIntegralValue(ONE);

    // reporta o valor monetário inicial
    sw.append("<p>Valor Inicial: <b>").append(currency.getSymbol());
    sw.append(' ').append(cf.format(kptal));
    sw.append("</b>&nbsp;(").append(currency.getName()).append(")</p>");

    // reporta datas que definem o período a planilhar
    sw.write("<div><table cellpadding=2 cellspacing=1><tr><td nowrap class=ini>Data Inicial:</td><td nowrap class=fim><b>");
    sw.format(DateUtils.localeBR, "%1$td de %1$tB de %1$tY", di);
    sw.write("</b></td></tr><tr><td nowrap class=ini>Data Final:</td><td nowrap class=fim><b>");
    sw.format(DateUtils.localeBR, "%1$td de %1$tB de %1$tY", df);
    sw.write("</b></td></tr>");
    // reporta o tempo decorrido entre datas
    sw.write("<tr><td nowrap class=ini>Tempo Decorrido:</td><td nowrap class=fim><b>");
    int k = (int) dateUtils.daysBetweenDates(di, df);
    sw.format("%,d dia", k);
    if (k > 1) sw.write('s');
    sw.write(" em ");
    if (NUM_ITER == 1) sw.write("único mês");
    else sw.format("%d meses", NUM_ITER);
    sw.write(".</b></td></tr></table></div>");

    // reporta taxa de juros e metodologia de cálculo a utilizar
    sw.write("<div><table cellpadding=2 cellspacing=1>");
    sw.write("<tr><td class=ini>Taxa de Juros:</td><td class=fim><b>");
    sw.append(pf.format(proRata)).append("<b> ao ano</td></tr>");
    if (validProRata) {
      sw.write("<tr><td class=ini>Regime de Capitalização:</td><td class=fim><b>");
      sw.write(getSelectedGroupOption(regimeGroup).getText().toUpperCase());
      sw.write("</b></td></tr>");
      sw.write("<tr><td class=ini>Agregação de Juros:</td><td class=fim><b>");
      sw.write(getSelectedGroupOption(agregacaoGroup).getText().toUpperCase());
      sw.write("</b></td></tr>");
    }
    sw.write("</table></div>");

    // reporta nome/descrição do índice de correção monetária
    sw.write("<p>Índice de Correção Monetária: <b>");
    sw.append(dbtables[ndx].name).append("</b>");
    // carrega propriedades das tabelas atualizáveis via webservice
    Properties h = loadProperties(DBUPDATE_PROPERTIES);
    // obtêm o código BCB da tabela em uso corrente
    String bcbcode = h.getProperty(dbtables[ndx].name);
    if (bcbcode != null) {
      // carregas as propriedades das tabelas disponíveis no BCB
      h = loadProperties(INDICES_MENSAIS_PROPERTIES);
      // obtêm a descrição da tabela em uso corrente
      String descricao = h.getProperty(bcbcode);
      if (descricao != null) {
        sw.append(" (<i>");
        // remove abreviação reduntante entre parentêses se existir
        Matcher m = Pattern.compile("\\s\\(([\\w-]+)\\)").matcher(descricao);
        if (m.find() && m.group(1).equalsIgnoreCase(dbtables[ndx].name)) {
          sw.append(descricao.substring(0, m.start()));
          sw.append(descricao.substring(m.end()));
        } else {
          sw.append(descricao);
        }
        sw.append("</i>)");
      }
    }
    sw.append("</p>");

    // inicia a planilha
    sw.write("<div class=main><table cellpadding=5 cellspacing=1>");
    sw.write("<tr><td colspan=2></td>");
    sw.write("<th colspan=3>Correção Monetária</td>");
    sw.write("<th colspan=3>Juros Remuneratórios</td>");
    sw.write("<td></td></tr><tr>");
    String[] headerList = {"Data", "Dias", "Taxa<br>Mensal", "Taxa<br>Aplicada", "Valor", "Taxa<br>Aplicada", "Valor", "Acumulado", "Capital<br>Acumulado"};
    for (String item: headerList) sw.format("<th nowrap>%s</td>", item);
    sw.write("</tr>");

    // vetor dos nomes de classes CSS alternantes a cada iteração do loop
    String[] className = { "one", "two" };

    // primeira linha da planilha
    mkRow(className[0], dateUtils.format(di), "<strong>Valor Inicial</strong>");

    // troca o formato de valores percentuais visando acessibilidade visual
    ((DecimalFormat) pf).applyPattern("#,##0.00##%;'<tt>'(#,##0.00##%)'</tt>'");

    // Statistical 'affairs' p/var.aleatória tempo de execução por iteração
    Stat<Double> stat = new Stat<Double>();
    // cronômetro de tempo de execução c/granularidade nanosegundos
    Chrono chrono = new Chrono();

    // widget visando responsividade ao apresentar progressão de cálculos
    Temporizador temporizador = new Temporizador(progressBar);

    // preserva o valor original do kptal
    okane = kptal;

    // inicializa as propriedades que totalizam valores monetários
    scorr = sjuros = jurosPending = jurosAno = ZERO;

    // inicializa as propriedades que acumulam taxas percentuais
    corrAcum = jurosAcum = ONE;

    // dia original do mês a realizar agregação de juros
    final int DAJ = di.get(Calendar.DATE);

    // verifica se a data inicial será ignorada no planilhamento
    // em consequência do dia dessa data ser o último do mês que
    // implicaria cálculos com zero dias efetivos
    final boolean skipFirst = (DateUtils.daysInMonth(di) == DAJ);

    // número de meses entre agregações de juros
    final int GAP = opAnual.isSelected() ? 12 : 6;

    // pesquisa de datas de agregação de juros
    class Diem
    {
      // ano, mês e dia da data pesquisada mais recente
      private int year, month, day;

      // contador do número de pesquisas realizadas
      private int n;

      // data mais recente como string
      private String dateStr;

      // status da desabilitação de pesquisas
      private boolean disabled;

      public Diem()
      {
        n = 0;
        disabled = false;
        next();
      }

      // pesquisa a próxima data de agregação
      public void next()
      {
        if (disabled) return;
        Calendar c = (Calendar) di.clone();
        // avança o calendário para o mês/ano da n-ésima agregação
        c.add(Calendar.MONTH, ++n * GAP);
        // verifica habilitação do modo leniente
        if (lenientDatesEnabled) {
          // ativa o modo leniente
          c.setLenient(true);
          // ajusta o dia do calendário
          c.set(Calendar.DATE, DAJ);
        }
        // obtêm os campos da data ajustada
        year = c.get(Calendar.YEAR);
        month = c.get(Calendar.MONTH);
        day = c.get(Calendar.DATE);
        // antecipa a formatação da data
        dateStr = dateUtils.format(c);
        // desabilita pesquisas se a data não é anterior à data final
        disabled = !c.before(df);
      }

      // retorna o dia da data pesquisada mais recente
      public int getDay() { return Diem.this.day; }

      // retorna a data mais recente como string
      public String asString() { return Diem.this.dateStr; }

      // verifica se há agregação de juros na data fornecida
      public boolean eligible(Calendar c)
      {
        return (c.get(Calendar.YEAR)  == Diem.this.year)
            && (c.get(Calendar.MONTH) == Diem.this.month)
            && (c.get(Calendar.DATE)  >= Diem.this.day);
      }
    }

    Diem diem = new Diem();

    // calendário do loop de planilhamento iniciado com a data inicial
    Calendar data = (Calendar) di.clone();

    // se não ignora o mês inicial do período de planilhamento então
    // decrementa o mês da data visando o ajuste inicial de sub-período
    if (!skipFirst) data.add(Calendar.MONTH, -1);

    // consulta SQL parametrizada a executar em cada iteração
    String q = buildSQL("valueFromDate", dbtables[ndx].name);

    try {

      PreparedStatement ps = connection.prepareStatement(q);

      temporizador.start();

      // loop de planilhamento da atualização monetária
      for (int m = (skipFirst ? 1 : 0); m < NUM_ITER; m++)
      {
        playAudioClip(6);
        chrono.start();
        k = m % 2;      // índice da classe CSS nessa iteração

        temporizador.update(((double) (m + 1)) / NUM_ITER);

        // AJUSTE DO SUB-PERÍODO

        // incrementa o mês nesta data
        data.add(Calendar.MONTH, 1);
        // número de dias no mês nesta data
        final int daysInMonth = DateUtils.daysInMonth(data);
        // último dia do mês nesta data
        final int lastDay = (m < NUM_ITER-1) ? daysInMonth : df.get(Calendar.DATE);

        // TAXA MENSAL DE CORREÇÃO MONETÁRIA

        // assinala o primeiro dia do mês nessa data
        data.set(Calendar.DATE, 1);
        // instancia um SQL Date com os milisegundos dessa data
        java.sql.Date date = new java.sql.Date(data.getTimeInMillis());
        // assinala o parâmetro da consulta SQL para obter a
        // taxa mensal de correção monetária correspondente
        if (useSQLite) ps.setString(1, date.toString());
        else ps.setDate(1, date);
        // executa a consulta SQL parametrizada
        ResultSet rs = ps.executeQuery();
        // valor integral da Taxa Mensal de Correção Monetária
        final BigDecimal txMensal = rs.next() ? new BigDecimal(rs.getString(1)) : null;
        rs.close();

        // assinala último dia do mês nesta data
        data.set(Calendar.DATE, lastDay);

        // número de dias no ano desta data
        final int daysInYear = DateUtils.daysInYear(data);

        // REFORMA MONETÁRIA

        final int r = Reforma.search(data);

        // dia do mês de reforma monetária :: -1 se não existe
        final int dr = (r > 0) ? Reforma.reformas[r].getDay() : -1;

        // verifica se neste mês há agregação de juros
        if (validProRata
            && ((m > 0 && m % GAP == 0) || (m > 1 && m % GAP == 1))
            && diem.eligible(data))
        {

          // dia efetivo de agregação de juros
          final int daj = diem.getDay();

          // verifica se é mês de reforma monetária e se o
          // dia é anterior ao dia de agregação dos juros
          if ((r > 0) && (dr < daj))
          {

            // juros & correção monetária desde o último dia do
            // mês anterior até o dia da reforma neste mês
            jcm(className[k], Reforma.reformas[r].getDate(), dr, txMensal, daysInMonth, daysInYear);

            scale(r); // aplica a reforma monetária

            // juros & correção monetária desde o dia da reforma
            // até o dia de agregação de juros
            jcm(className[k], diem.asString(), daj - dr, txMensal, daysInMonth, daysInYear);

          } else {

            // juros & correção monetária desde o último dia do mês
            // anterior até o dia de agregação de juros neste mês
            jcm(className[k], diem.asString(), daj, txMensal, daysInMonth, daysInYear);

          }

          // AGREGAÇÃO DOS JUROS REMUNERATÓRIOS

          // agrega a soma dos juros remuneratórios pendentes ao capital
          kptal = kptal.add(jurosPending);
          // reinicia a soma dos juros remuneratórios pendentes
          jurosPending = ZERO;
          // acumula a soma de taxas de juros remuneratórios pendentes
          jurosAcum = jurosAcum.multiply(jurosAno.add(ONE));
          // reinicia a soma de taxas de juros remuneratórios pendentes
          jurosAno = ZERO;
          // reporta a agregação de juros
          mkRow(className[k], diem.asString(), "Agregação de Juros Remuneratórios");

          // SUB-PERÍODO COMPLEMENTAR PÓS DATA DE AGREGAÇÃO

          if (daj <= lastDay)
          {

            // verifica se é mês de reforma monetária e se o dia
            // ocorre entre o dia de agregação e o último dia do mês
            if ((r > 0) && (daj <= dr) && (dr <= lastDay))
            {

              // juros & correção monetária desde o dia de agregação
              // de juros até o dia da reforma se possível
              if (dr > daj) { // not redundant!

                jcm(className[k], Reforma.reformas[r].getDate(), dr - daj, txMensal, daysInMonth, daysInYear);

              }

              scale(r); // aplica a reforma monetária

              // juros & correção monetária desde o dia da reforma
              // até o último dia do mês se possível
              if (lastDay > dr) { // not redundant!

                jcm(className[k], data, lastDay - dr, txMensal, daysInMonth, daysInYear);

              }

            } else {

              // juros & correção monetária desde o dia de agregação
              // de juros até o último dia do mês se possível
              if (lastDay > daj) {

                jcm(className[k], data, lastDay - daj, txMensal, daysInMonth, daysInYear);

              }

            }

          }

          diem.next(); // prepara a próxima data de agregação de juros

        } else {

          // verifica se é mês de reforma monetária e se o dia de sua
          // aplicação não é anterior ou igual ao dia da data inicial
          // na iteração 'zero' e se o dia não é posterior ao último
          // dia do mês na última iteração
          if ((r > 0) && !(m == 0 && dr <= DAJ) && !(m == NUM_ITER-1 && lastDay < dr))
          {

            // juros & correção monetária desde o dia anterior ao dia
            // inicial até o dia da reforma
            jcm(className[k], Reforma.reformas[r].getDate(), ((m > 0) ? dr : dr - DAJ), txMensal, daysInMonth, daysInYear);

            scale(r); // aplica a reforma monetária

            // juros & correção monetária desde o dia da reforma
            // até o último dia do mês se possível
            if (lastDay > dr) {

              jcm(className[k], data, lastDay - dr, txMensal, daysInMonth, daysInYear);

            }

          } else {

            // juros & correção monetária para os dias efetivos deste mês
            jcm(className[k], data, ((m > 0) ? lastDay : lastDay - DAJ), txMensal, daysInMonth, daysInYear);

          }

        }

        // LAP TIME STATISTICS
        chrono.stop();
        stat.add(chrono.time());
      }

      ps.close();

    } catch (SQLException e) {
      System.err.println(e);
    }

    sw.format("<tr><td nowrap class=%s_center>TOTAL</td>", className[k]);
    q = String.format("<td class=%s></td>", className[k]);
    for (int i=0; i < 3; i++) sw.write(q);
    String s = String.format("<td nowrap class=%s>%%s</td>", className[k]);
    // reporta o somatório das correções monetárias
    sw.format(s, cf.format(scorr));
    sw.write(q);
    // reporta o somatório dos juros remuneratórios
    sw.format(s, cf.format(sjuros));
    sw.append(q).append(q).append("</tr>");

    // data final como string formatada
    String dfString = dateUtils.format(df);

    // possível última linha da planilha
    mkRow(className[k], dfString, "<strong>Valor Atualizado</strong>");

    // nota de rodapé sobre o padrão monetário na data final
    sw.append("<tr><td colspan=9 class=rem>Padrão Monetário em ");
    sw.append(dfString).append(": <b>");
    sw.append(currency.getName()).append("</b> (<b>");
    sw.append(currency.getSymbol()).append("</b>).</td></tr>");

    // verifica se há juros remuneratórios pendentes dado
    // que a data final não coincide com data de agregação
    boolean delayed = validProRata && (jurosPending.compareTo(ZERO) > 0);

    if (delayed)
    {
      // verifica a magnitude dos juros pendentes tal que o valor
      // seja no mínimo de um centavo, ainda que por arredondamento
      // caso contrário geraria um mal estar por apresentar "zero"
      if (jurosPending.compareTo(EPS) >= 0) {
        s = String.format("Juros Remuneratórios pendentes até %s", diem.asString());
      } else {
        s = String.format("Juros Remuneratórios pendentes até %s<br>menores que %s 0,01", diem.asString(), currency.getSymbol());
      }
      mkRow("xtra0", dfString, s, jurosPending);

      // agrega os juros remuneratórios pendentes ao capital
      kptal = kptal.add(jurosPending);
      kptal = kptal.setScale(2, RoundingMode.HALF_UP);

      mkRow("xtra1", dfString, "Valor Atualizado + Juros Remuneratórios pendentes");

      // totaliza a taxa acumulada de juros remuneratórios em pendência
      jurosAcum = jurosAcum.multiply(jurosAno.add(ONE));
    }

    // fim do planilhamento
    sw.write("</table></div>");

    // ----- TABELA DOS PERCENTUAIS ACUMULADOS -----

    pf.setMinimumFractionDigits(4);
    sw.write("<div><table cellpadding=5 cellspacing=1>");
    sw.write("<tr><th colspan=2 nowrap>Taxas Acumuladas no Período</td></tr>");

    sw.write("<tr><td class=one_left>Correção Monetária (<b>");
    sw.write(dbtables[ndx].name);
    sw.write("</b>)</td><td class=one>");
    sw.append(pf.format(corrAcum.subtract(ONE))).append("</td></tr>");

    sw.write("<tr><td class=two_left>Juros Remuneratórios</td><td class=two>");
    sw.append(pf.format(jurosAcum.subtract(ONE))).append("</td></tr>");
    sw.write("</table></div>");

    // verifica se o percentual acumulado de juros remuneratórios é maior
    // que zero e se o percentual acumulado de correção monetária não é
    // igual a zero como condição de viabilidade do cálculo do percentual
    // de juros remuneratórios efetivo sem aplicação de correção monetária
    if (validProRata && (jurosAcum.compareTo(ONE) > 0)
        && (corrAcum.compareTo(ONE) != 0))
    {
      sw.write("<div class=remark><table cellpadding=5><tr>");
      sw.write("<td nowrap valign=top><b>Observação</b>:</td><td>");
      sw.write("Sem Correção Monetária os Juros Remuneratórios seriam ");
      // jurosAcum / corrAcum - 1
      BigDecimal x = jurosAcum.divide(corrAcum, mc).subtract(ONE);
      sw.append(pf.format(x)).append(".</td></tr></table></div>");
    }

    // ----- TABELA RESUMO DOS VALÔRES COMPUTADOS -----

    pf.setMaximumFractionDigits(2);
    sw.write("<div><table cellpadding=5 cellspacing=1>");
    sw.write("<tr><th colspan=3 nowrap>Resumo dos Valôres Computados</td></tr>");

    sw.write("<tr><td class=one_left>Correção Monetária</td>");
    s = "<td class=one>%s</td>";
    sw.format(s, cf.format(scorr));

    // kptal <= okane ? :: valor atualizado <= valor inicial ?
    boolean fault = (kptal.subtract(okane).compareTo(ZERO) <= 0);

    if (fault) sw.write("<td class=one_center>---</td>");
    else {
      // scorr / (kptal - okane)
      BigDecimal y = scorr.divide(kptal.subtract(okane), mc);
      sw.format(s, pf.format(y));
    }
    sw.write("</tr>");

    sw.write("<tr><td class=two_left>Juros Remuneratórios</td>");
    q = "<td class=two>%s</td>";
    sw.format(q, cf.format(kptal.subtract(scorr).subtract(okane)));
    // kptal <= okane
    if (fault) sw.write("<td class=two_center>---</td>");
    else {
      // x = (kptal - okane - scorr) / (kptal -okane)
      BigDecimal y = kptal.subtract(okane);
      y = y.subtract(scorr).divide(y, mc);
      sw.format(q, pf.format(y));
    }
    sw.write("</tr>");

    sw.write("<tr><td class=one_center>TOTAL</td>");
    sw.format(s, cf.format(kptal.subtract(okane)));
    // kptal > okane
    if (!fault) sw.write("<td class=one>100,00%</td>");
    else sw.write("<td class=one_center>---</td>");
    sw.write("</tr>");

    sw.write("<tr><td class=two_left>Valor Inicial</td>");
    sw.format(q, cf.format(okane));
    sw.write("<td class=two_center>");
    sw.append(dateUtils.format(di)).append("</td></tr>");

    sw.write("<tr><td class=one_left>Valor Atualizado</td>");
    sw.format(s, cf.format(kptal));
    sw.write("<td class=one_center>");
    sw.append(dfString).append("</td></tr>");

    sw.write("<tr><td nowrap colspan=3 class=rem>");
    sw.write("Padrão Monetário: <b>");
    sw.append(currency.getName()).append("</b> (<b>");
    sw.append(currency.getSymbol()).append("</b>).</td></tr>");

    sw.write("</table></div>");

    sw.write("<p>Valor Atualizado");
    // evita declaração contraditória ou incompleta
    if (delayed) sw.append(" + Juros&nbsp;Remuneratórios pendentes");
    sw.append(": <b>").append(currency.getSymbol());
    sw.append("&nbsp;").append(cf.format(kptal));
    sw.append("</b>&nbsp;(").append(currency.getName()).append(")</p>");

    execChrono.stop(); // UFF!

    // ---- ESTATÍSTICAS DO PROCESSAMENTO

    sw.write("<div><table cellpadding=2 cellspacing=1>");
    sw.write("<tr><td class=mer>Número de Iterações:</td><td class=rem>");
    sw.format("<b>%d</b></td></tr>", stat.getSize());
    sw.write("<tr><td class=mer>Tempo Total de Execução (s):</td>");
    sw.format(DateUtils.localeBR, "<td class=rem><b>%.3f", execChrono.time());
    sw.write("</b></td></tr>");
    sw.write("<tr><td class=mer>Tempo Total das Iterações (s):</td>");
    sw.format(DateUtils.localeBR, "<td class=rem><b>%.3f", stat.getTotal());
    sw.write("</b></td></tr>");
    sw.write("<tr><td class=mer>Tempo Médio / Iteração (s):</td>");
    sw.format(DateUtils.localeBR, "<td class=rem><b>%.3f", stat.getMean());
    sw.format(DateUtils.localeBR, " &#177; %.3f", stat.getStdDeviation());
    sw.write("</b></td></tr><tr><td colspan=2 class=rem><br>");
    sw.format(DateUtils.localeBR, "Gerado em %tc.", System.currentTimeMillis());
    sw.write("</td></tr></table></div></body></html>");
    sw.flush();

    // exibe o conteúdo do buffer na área de reportagem
    clearReport();
    report.getEditorKit().createDefaultDocument();
    report.setContentType("text/html");
    report.setText(sw.toString());
    assureSplitPaneDisplay(true);
    report.requestFocusInWindow();

    temporizador.terminate();

    playAudioClip(2);
    setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));

    adjustFormater(true);

    return true;
  }

  /**
   * Inicia o aplicativo.
   *
   * @param args Array de argumentos.
  */
  public static void main(String[] args)
  {
    splashHelper = new SplashHelper("Construindo a interface.");
    JCM f = new JCM();
    f.pack();
    splashHelper.close();
    f.setVisible(true);
  }

  /** Gerenciador do SplashScreen para qualquer versão de JRE. */
  private static SplashHelper splashHelper;

  /** Writer extendido para facilitar uso de formatadores. */
  private class CustomWriter extends StringWriter
  {
    /**
     * Escreve conteúdo no final do buffer com formatação para localidade
     * especificada.
     *
     * @param locale Localidade específica na formatação.
     * @param fmt Formato a ser aplicado nos argumentos.
     * @param args Argumentos a escrever no final do buffer.
    */
    public void format(Locale locale, String fmt, Object... args)
    {
      write(String.format(locale, fmt, args));
    }

    /**
     * Escreve conteúdo no final do buffer com formatação.
     *
     * @param fmt Formato a ser aplicado nos argumentos.
     * @param args Argumentos a escrever no final do buffer.
    */
    public void format(String fmt, Object... args)
    {
      write(String.format(fmt, args));
    }
  }

  /** Buffer de montagem do documento contendo a planilha. */
  private CustomWriter sw;

  /** Formatador de valores monetários da planilha. */
  private NumberFormat cf;

  /** Formatador de valores percentuais da planilha. */
  private NumberFormat pf;

  /** Montante objeto da atualização monetária. */
  private BigDecimal kptal;

  /** Montante preservado para comparação ao final da atualização. */
  private BigDecimal okane;

  /** Somatório das correções monetárias. */
  private BigDecimal scorr;

  /** Somatório dos juros. */
  private BigDecimal sjuros;

  /** Somatório dos juros pendentes até data de agregação. */
  private BigDecimal jurosPending;

  /** Taxa de correção monetária acumulada no período. */
  private BigDecimal corrAcum;

  /** Taxa de juros remuneratórios acumulado no período. */
  private BigDecimal jurosAcum;

  /** Taxa de juros remuneratórios acumulado no ano. */
  private BigDecimal jurosAno;

  /** PRO-RATA : Taxa de Juros Remuneratórios Anual a cada planilhamento. */
  private BigDecimal proRata;

  /** Efetividate de cálculos com a taxa PRO-RATA a cada planilhamento. */
  private boolean validProRata;

  /** Encapsulamento de vários métodos usuais de calendário. */
  private DateUtils dateUtils;

  /** Engine de pesquisa do padrão monetário nas datas. */
  private Moeda currency;

  /** Habilitação dos centavos conforme moeda da época. */
  private boolean useCents;

  /** Mínimo valor monetário que pode ser arredondado para um centavo. */
  private static final BigDecimal EPS = new BigDecimal("0.005");

  /**
   * Nome do arquivo de propriedades contendo todos os pares código/nome dos
   * índices de atualização monetária disponíveis no SGS do BCB.
  */
  private static final String INDICES_MENSAIS_PROPERTIES = "indicesmensais.properties";

  /**
   * Nome do arquivo de propriedades contendo os pares nome/código dos índices de
   * atualização monetária em uso corrente que são atualizáveis online.
  */
  private static final String DBUPDATE_PROPERTIES = "dbupdate.properties";

  /**
   * Nome do arquivo de propriedades contendo os pares usuário/senha
   * para acesso ao banco de dados marca Derby ou MySQL.
  */
  private static final String JCMDB_PROPERTIES = "jcmdb.properties";

  private static final long serialVersionUID = -1759470129035495977L;
}